using System;
using System.Collections.Generic;
using System.Text;
using System.CodeDom.Compiler;
using System.CodeDom;
using SharpMud.Server.Systems.Scripting.Processing.Source;

namespace SharpMud.Server.Systems.Scripting.Processing
{
    internal sealed partial class ScriptCompileContext : IScriptCompileContext
    {
        object _CompileSyncSink;

        #region CompileScripts - Top Level - Overloads
        private KeyValuePair<IScriptSourceHandle, IScriptCompileResults>[] CompileScripts()
        { return CompileScripts(FilterErroneousScripts_ByDefault); }

        private KeyValuePair<IScriptSourceHandle, IScriptCompileResults>[] CompileScripts(bool filterErroneousScripts)
        {
            List<KeyValuePair<IScriptSourceHandle, IScriptCompileResults>> grandResults;
            grandResults = new List<KeyValuePair<IScriptSourceHandle, IScriptCompileResults>>();

            //Do a synclock to prevent simultaneous compiles by the same object
            lock (_CompileSyncSink)
            {
                //Subdivide the scripts into per-language sets
                KeyValuePair<System.Type, IList<IScriptSourceHandle>>[] subdividedScripts;
                subdividedScripts = SubdivideScriptsByLanguage();
                //Compile each per-language set of scripts
                foreach (KeyValuePair<System.Type, IList<IScriptSourceHandle>> languageScriptSet in subdividedScripts)
                {
                    bool hadErrors = false;
                    CompilerResults compilerResults;

                    if (filterErroneousScripts)
                    { //Compile, and make sure we do it right!
                        //Deduce faulty scripts until a compile is successful or there 
                        //are no scripts left
                        do
                        {
                            compilerResults = this.CompileSubdividedScripts(languageScriptSet).Key;
                            hadErrors = compilerResults.Errors.HasErrors;

                            if (hadErrors)
                            {//Search compiler errors and figure out what to "fix"
                                //This method call finds the bad script, gathers messages about it,
                                //and removes the script from the list of scripts to compile
                                KeyValuePair<IScriptSourceHandle, IScriptCompileResults> erroneousScript = this.EliminateFirstCompilerError(languageScriptSet.Value, compilerResults);
                                //We add the script source, and the compile results for it, to the grandResults
                                grandResults.Add(new KeyValuePair<IScriptSourceHandle, IScriptCompileResults>(erroneousScript.Key, erroneousScript.Value));
                            }
                            //If errors, loop and try compile again, this time, without that
                            // erroneous script we detected.
                            //If no errors, the do..while and the big if..else both break;
                        } while (hadErrors && filterErroneousScripts);
                    }
                    else
                    { //Try and compile once, and then give up...
                        compilerResults = this.CompileSubdividedScripts(languageScriptSet).Key;
                        hadErrors = compilerResults.Errors.HasErrors;
                    }//Done compiling, either way

                    foreach(IScriptSourceHandle script in languageScriptSet.Value)
                    {
                        grandResults.Add(new KeyValuePair<IScriptSourceHandle, IScriptCompileResults>(script, CollectCompileResults(script, compilerResults)));
                    }

                }//Done compiling each language-set of scripts
            }//Done with synclock
            //Transform the grandResults into a form suiting the method signature
            return grandResults.ToArray();
        }
        #endregion

        #region Compile - Middle Level - Wheat and Chaffe
        /// <summary>
        /// Find the first erroneous script and gather its messages, and remove it from the list
        /// of scripts given
        /// </summary>
        private KeyValuePair<IScriptSourceHandle, IScriptCompileResults> EliminateFirstCompilerError(IList<IScriptSourceHandle> scripts, CompilerResults compilerResults)
        {
            if (scripts == null)
                throw new ArgumentNullException("scripts");
            if (compilerResults == null)
                throw new ArgumentNullException("compilerResults");
            System.Diagnostics.Debug.Assert(compilerResults.Errors.HasErrors, "compilerResults.Errors.HasErrors");

            ScriptCompileResults scr = new ScriptCompileResults();
            IScriptSourceHandle erroneousScript = null ;

            foreach (CompilerError compilerError in compilerResults.Errors)
            {
                foreach (IScriptSourceHandle script in scripts)
                {
                    if ((script as ICompilerErrorTransformer).IsSource(compilerError, script))
                    {
                        erroneousScript = script;
                        break;
                    }
                }
                if (erroneousScript != null)
                    break;
            }
            System.Diagnostics.Debug.Assert(erroneousScript != null, "erroeneousScript!=null");
            IScriptCompileResults erroneousScriptCompileResults;
            erroneousScriptCompileResults = CollectCompileResults(erroneousScript, compilerResults);
            scripts.Remove(erroneousScript);
            return new KeyValuePair<IScriptSourceHandle, IScriptCompileResults>(erroneousScript, erroneousScriptCompileResults);
        }

        private IScriptCompileResults CollectCompileResults(IScriptSourceHandle script, CompilerResults compilerResults)
        {
            if (script == null)
                throw new ArgumentNullException("script");
            if (compilerResults == null)
                throw new ArgumentNullException("compilerResults");
            bool failed = false;
            ScriptCompileResults scriptCompileResults = new ScriptCompileResults();
            List<CompilerError> relevantCompilerErrors = new List<CompilerError>();
            foreach (CompilerError compilerError in compilerResults.Errors)
            {
                if ((script as ICompilerErrorTransformer).IsSource(compilerError, script))
                {
                    if (!compilerError.IsWarning)
                        failed = true;
                    relevantCompilerErrors.Add(compilerError);
                }
            }
            scriptCompileResults._CompileMessages.AddRange((script as ICompilerErrorTransformer).ProcessCompilerErrors(relevantCompilerErrors.ToArray(), script));

            if (failed)
            {
                return scriptCompileResults;
            }
            else
            { //If compile *didn't* fail, then find that handle...
                ICompiledScriptHandle compiledScriptHandle = CompiledScriptHandle.FromSourceAndTypes(new List<System.Type>(compilerResults.CompiledAssembly.GetTypes()),script);
                System.Diagnostics.Debug.Assert(compiledScriptHandle != null, "compiledScriptHandle!=null");
                scriptCompileResults._CompiledScriptHandle = compiledScriptHandle;
                //...and return everything
                return scriptCompileResults;
            }
        }
        #endregion

        #region Compile - Bottom Level - Divide And Conquor

        private KeyValuePair<CompilerResults, IList<IScriptSourceHandle>> CompileSubdividedScripts(KeyValuePair<System.Type, IList<IScriptSourceHandle>> languageScriptSet)
        {
            System.Type codeDomProviderType = languageScriptSet.Key;

            if (codeDomProviderType == null)
                throw new ArgumentNullException("languageScriptSet.Key", "A null CodeDomProvider type was provided.");

            if (codeDomProviderType != null && !(typeof(CodeDomProvider).IsAssignableFrom(codeDomProviderType)))
                throw new InvalidOperationException("A set of IScriptSourceHandle's are inappropriately reporting " + codeDomProviderType.AssemblyQualifiedName + " as their preferred CodeDomProvider.");

            IList<IScriptSourceHandle> scripts = languageScriptSet.Value;
            CodeDomProvider codeDomProvider = (CodeDomProvider)codeDomProviderType.Assembly.CreateInstance(codeDomProviderType.FullName, false);

            List<CodeCompileUnit> codeCompileUnits = new List<CodeCompileUnit>();

            //Filter out already compiled scripts
            foreach (IScriptSourceHandle script in scripts)
            {
                if (script.IsDirty == true || (script.IsDirty==false && this.GetLastCompileResults(script)==null) )
                {
                    codeCompileUnits.Add(script.GetCompileUnit());
                }
            }

            CompilerParameters compilerParameters = this.GenerateCompilerParameters();
            CompilerResults compilerResults = codeDomProvider.CompileAssemblyFromDom(compilerParameters, codeCompileUnits.ToArray());

            return new KeyValuePair<CompilerResults, IList<IScriptSourceHandle>>(compilerResults, scripts);
        }

        private KeyValuePair<System.Type, IList<IScriptSourceHandle>>[] SubdivideScriptsByLanguage()
        {
            Dictionary<System.Type, IList<IScriptSourceHandle>> divList = new Dictionary<System.Type, IList<IScriptSourceHandle>>();

            lock (this._Scripts)
            {
                foreach (IScriptSourceHandle script in this._Scripts)
                {
                    if (script.PreferredLanguage != null && !(typeof(CodeDomProvider).IsAssignableFrom(script.PreferredLanguage)))
                    {
                        throw new InvalidOperationException("An IScriptSourceHandle is inappropriately reporting " + script.PreferredLanguage.AssemblyQualifiedName + " as its preferred CodeDomProvider.");
                    }
                    if (!divList.ContainsKey(script.PreferredLanguage))
                    {
                        IList<IScriptSourceHandle> sList = (IList<IScriptSourceHandle>)new List<IScriptSourceHandle>();
                        sList.Add(script);
                        divList.Add(script.PreferredLanguage, sList);
                    }
                    else // if(divList.ContainsKey(script.PreferredLanguage))
                    {
                        divList[script.PreferredLanguage].Add(script);
                    }
                }
            }
            List<KeyValuePair<System.Type, IList<IScriptSourceHandle>>> listedDic = new List<KeyValuePair<Type, IList<IScriptSourceHandle>>>();
            foreach (System.Type t in divList.Keys)
            {
                listedDic.Add(new KeyValuePair<Type, IList<IScriptSourceHandle>>(t, divList[t]));
            }
            return listedDic.ToArray();
        }

        #endregion
    }
}